package edu.mercer.sse.ldap;

import edu.mercer.sse.app.Config;

import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldif.LDIFException;

import edu.mercer.sse.errors.CriticalException;
import edu.mercer.sse.errors.CustomLogger;
import edu.mercer.sse.errors.ErrorLoggingVisitor;
import edu.mercer.sse.errors.LoggerCustom;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * 
 * LDAP Group of Unique Names: list, add, remove members.<p>
 *
 * Maintains cached version LDAP group of unique names. Refreshes when 
 * 	modified using {@link #removeMember(String)}, {@link #addMember(String)} 
 * 	methods.<p>
 *
 * Removal method also performs validation of members, removing any members no 
 * 	longer found in ldap.<p>
 *
 */

public class LDAPGroupUniqNames
{
	private static Optional<ImmutableList<String>> _names = Optional.absent();

	private static Map<String,String[]> _attributes = new HashMap<>();
	
	private static final CustomLogger _log = LoggerCustom.obtainLog( 
		
		"LDAPGroupUniqNames", 
			
		Config.getLogLevel());

	
	/**
	 * 
	 * LDAP Group of Unique Names: list, add, remove members.<p>
	 *
	 * No-arg constructor.<p>
	 *
	 * Maintains cached version LDAP group of unique names. Refreshes when 
	 * 	modified using {@link #removeMember(String)}, 
	 * 	{@link #addMember(String)} methods.<p>
	 *
	 * Removal method also performs validation of members, removing any 
	 * 	members no longer found in ldap.<p>
	 *
	 */
	
	LDAPGroupUniqNames(){}
	
	
	private static ImmutableList<String> retrieveMembers() throws 
			LDAPSearchException, 
			LDAPException, 
			GeneralSecurityException
	{
		clear();
		
		final Iterator<Attribute> attrsFromLdap = 

				LDAPConnector.con().search
				(
					Config.settings().getProperty( "ldap-memberof" ),
					SearchScope.SUB,
					Filter.create( Config.settings().getProperty( 
							"ldap-entries-objectclass") )
				)
				.getSearchEntries()
				.iterator()
				.next()
				.getAttributes()
				.iterator();

		checkArgument( attrsFromLdap.hasNext(), "empty attributes retrieved "+
				"for group of names: " 
				+ Config.settings().getProperty( "ldap-memberof" ) );

		while( attrsFromLdap.hasNext() )
		{
			Attribute attr = attrsFromLdap.next();
			
			_attributes.put( attr.getBaseName(), attr.getValues() );
			
		}//while
		
		checkArgument( _attributes.containsKey( Config.settings()
				.getProperty( "ldap-member-key" )), 
				"failed to find ldap attribute: " + Config.settings()
				.getProperty( "ldap-member-key" ));
		
		/* uniqueMemberOf String array of member full UID's DN's */
		
		_names = Optional.of( ImmutableList.copyOf( 
								_attributes.get( Config.settings()
										.getProperty( "ldap-member-key" ))));

		return _names.get();
		
	}//method
	
	
	public static Boolean removeMember( final String memberUID ) 
	{
		Boolean result = false;
		
		try
		{		
			if( ! isMember(memberUID) )
			{
				_log.warning( "removal fail, not in group of names: " + 
						memberUID );

				return false;

			}//if

			/* download member list only if required */

			String[] membersArr = _names.or( retrieveMembers() )
					.toArray(new String[1]);

			List<String> membersList = Lists.newArrayList( membersArr );

			/* validate member list, ensuring each member exists in ldap */

			Iterator<String> userDNs = membersList.iterator();

			List<String> usersToKull = Lists.newArrayList();
			
			while( userDNs.hasNext() )
			{
				String user = userDNs.next();
				
				checkNotNull( user, "null user DN encountered in group of "
						+ "names" );
				
				if( ! LDAPUtility.isPresent( user ) )
				{
					_log.warning("user: " + user + " missing from ldap " 
							+ "but listed in group of names");
					
					usersToKull.add( user );
					
				}//if
				
			}//while
			
			if( usersToKull.size() > 0 ) membersList.removeAll( usersToKull );
			
			/* remove from list of names */

			if( membersList.contains( memberUID ) )
			{
				/* 
				 * if invalid DN according to ldap, may have been removed 
				 * already from list in above removeAll logic
				 */
				
				checkArgument(membersList.remove( memberUID ), 
						"did not remove: " + memberUID + " from group of names: " 
							+ _names);
				
			}//if
			
			String[] updatedMembersArr = membersList.toArray(
					new String[membersList.size()]);

			/* replace member list array of names, having removed the name */

			_attributes.put( Config.settings().getProperty(
					"ldap-member-key"), updatedMembersArr );

			String attrLog = "";

			for( String attributeKey : _attributes.keySet() ) attrLog += 
					attributeKey 
					+ "=" 
					+ Lists.newArrayList(_attributes.get(attributeKey)) 
					+ "\n";  

			_log.fine( "removal of " + memberUID + ", writing to group: " 
					+ attrLog );

			result = LDAPUtility.writeEntry( 
						Config.settings().getProperty( "ldap-memberof" ), 
						_attributes, 
						"overwrite" );

			refresh();
		
		}
		catch( final LDAPException|GeneralSecurityException|LDIFException err)
		{
			new CriticalException(err).accept( new ErrorLoggingVisitor() );			
			
		}//try
		
		return result;
		
	}//method
	
	
	public static Boolean addMember( final String memberDN )
			throws LDAPSearchException, 
				   LDAPException, 
				   GeneralSecurityException, 
				   LDIFException
	{
		checkNotNull(memberDN, "null member uid");
		
		if( isMember(memberDN) )
		{
			_log.warning( "already in group of names: " + memberDN );
			return false;
			
		}//if

		/* download member list only if required */
		
		String[] membersArr = _names.or( retrieveMembers() )
				.toArray(new String[1]);
		
		List<String> membersList = Lists.newArrayList( membersArr );
		
		membersList.add( memberDN );

		String[] updatedMembersArr = membersList.toArray(
				new String[membersList.size()]);
		
		_attributes.put( Config.settings().getProperty(
				"ldap-member-key"), updatedMembersArr );
		
		String attrLog = "";
		
		for( String attributeKey : _attributes.keySet() ) attrLog += 
				attributeKey 
				+ "=" 
				+ Lists.newArrayList(_attributes.get(attributeKey)) 
				+ "\n";  
		
		_log.fine( "writing to group: " + attrLog );
		
		Boolean result = LDAPUtility.writeEntry( Config.settings().getProperty( 
				"ldap-memberof" ), 
				_attributes, 
				"overwrite" );

		refresh();
		
		return result;
		
	}//method

	
	public static Boolean isMember( String memberDN )
		throws LDAPSearchException, 
			   LDAPException, 
			   GeneralSecurityException
	{
		checkNotNull( memberDN, "null member dn" );

		checkArgument( memberDN.length() > 0, "empty member uid" );

		/* remove any supplied dn, base, etc. */
		
		memberDN = memberDN.split(",")[0]
					+ "," +
					Config.settings().getProperty( "ldap-entries-ou" ) 
					+ "," +
					Config.settings().getProperty( "ldap-base-dn" ); 
		
		/* prepend uid= if needed */
		
		if( ! memberDN.startsWith( Config.settings().getProperty(
				"ldap-member-id") + "=" ) )
		{
			memberDN = Config.settings().getProperty("ldap-member-id") 
					+ "=" 
					+ memberDN;
			
		}//if
		
		Boolean found = false;
		
		found = _names.or( retrieveMembers() ).contains(memberDN);
		
		_log.fine( memberDN 
				+ " seek result: '" 
				+ found 
				+ "' in " 
				+ _names.get()); 

		return found;
		
	}//method
	
	
	private static void clear()
	{
		_attributes.clear();
		_names = Optional.absent();
	
	}//method
	
	
	private static void refresh() throws LDAPSearchException, 
										 LDAPException, 
										 GeneralSecurityException
	{
		clear();
		retrieveMembers();
		
	}//method
	
	
}//class
